from django.db import models
from django.db.models import permalink, Sum, Count
from django.db.models.signals import post_save, post_delete
from django.contrib.auth.models import User
from student.models import Student
from course.models import Course
from schedule.models import Offering, OFFERING_TYPE_CHOICES
from operator import attrgetter
from collections import defaultdict
from django.core.signals import got_request_exception 

TES = ('math', 'sci', 'engsci', 'engdes')
CSE = ('a', 'b', 'c', 'd')
PLAN_STATES = (
    (1, 'In Progress'),
    (2, 'Submitted'),
    (3, 'Approved'),
    (4, 'Denied'),
)

TERMS = zip(
    range(1,9),
    ('1A', '1B', '2A', '2B', '3A', '3B', '4A', '4B')
)

class Plan(models.Model):
    student = models.ForeignKey(Student)
    courses = models.ManyToManyField(Course, through='PlannedCourse')
    errors = models.TextField(blank=True)
    state = models.PositiveIntegerField(choices=PLAN_STATES, default=1)
    created_at = models.DateTimeField(auto_now_add = True)
    updated_at = models.DateTimeField(auto_now = True)
    submitted_at = models.DateTimeField(blank=True, null=True)

    @classmethod
    def initialize_plan(cls, student):
        p = cls(student = student)
        p.save()

        core_courses = Course.objects.filter(core_course = True)
        print core_courses

        for c in core_courses:
            pc = PlannedCourse(plan = p, course = c, term = c.default_term)
            pc.save()

    def __unicode__(self):
        return "%s (%s)" % (self.student.full_name, self.id)

    def is_owner(self, user):
        """
        Expects a User object and if the associated
        student owns the plan, return True, otherwise return
        False.
        """
        # check type for security reasons
        return type(user) == User and self.student_id == user.student_set.get().id

    @permalink
    def get_absolute_url(self):
        return ('plan_detail', [str(self.id)])

    @property
    def requirements(self):
        return self.student.degree_requirement

    @property
    def num_courses(self):
        return self.courses.count()

    def min_courses_per_term(self):
        def count_courses(memo, course):
            memo[course] += 1
            return memo

        c = [c.term for c in self.plannedcourse_set.all()]
        course_count = reduce(count_courses, c, defaultdict(lambda: 0))
        return min(course_count.values())

    def te_hours(self, category):
        if not category in TES:
            raise "Invalid TE category"

        te_getter = attrgetter('te_' + category)

        return reduce(lambda m, x: m + te_getter(x), self.courses.all(), 0)

    def all_te_hours(self):
        return self.courses.aggregate(sci=Sum('te_sci'), math=Sum('te_math'), engdes=Sum('te_engdes'), engsci=Sum('te_engsci'))
            

    def cse_count(self, category):
        if not category in CSE:
            raise "Invalid CSE category"

        cse_getter = attrgetter('cse_' + category)

        return len(filter(lambda x: cse_getter(x) > 0, self.courses.all()))

    def quicker_cse_count(self, category):
        if not category in CSE:
            raise "Invalid CSE category"
        kwargs = {}
        kwargs['cse_%s__gt' % category] = 0
        return self.courses.filter(**kwargs).count()

    def get_errors(self):
        def pluralize(str, num):
            if num > 1:
                return str + 's'
            else:
                return str

        english_errors = []

        errors = self.validate_plan()

        # This is sort of ugly
        for e in errors:
            if e[0] == 'too_few_courses':
                english_errors.append("You must take 5 courses in each academic term.")
            elif e[0] == 'math_too_low':
                english_errors.append("You must take %s additional hours of math credits." % e[1])
            elif e[0] == 'sci_too_low':
                english_errors.append("You must take %s additional hours of science credits." % e[1])
            elif e[0] == 'engsci_too_low':
                english_errors.append("You must take %s additional hours of engineering science credits." % e[1])
            elif e[0] == 'engdes_too_low':
                english_errors.append("You must take %s additional hours of engineering design credits." % e[1])
            elif e[0] == 'math_sci_too_low':
                english_errors.append("You must take %s additional hours of combined math and science credits." % e[1])
            elif e[0] == 'engsci_des_too_low':
                english_errors.append("You must take %s additional hours of combined engineering science and design credits." % e[1])
            elif e[0] == 'too_few_tes':
                english_errors.append("You must take %d additional %s." % (e[1], pluralize('TE', e[1])))
            elif e[0] == 'list_a_too_low':
                english_errors.append("You must take %s additional List A %s." % (e[1], pluralize('CSE', e[1])))
            elif e[0] == 'list_b_too_low':
                english_errors.append("You must take %s additional List B %s." % (e[1], pluralize('CSE', e[1])))
            elif e[0] == 'list_c_too_low':
                english_errors.append("You must take %s additional List C %s." % (e[1], pluralize('CSE', e[1])))
            elif e[0] == 'too_few_cses':
                english_errors.append("You must take %d additional %s." % (e[1], pluralize('CSE', e[1])))

        return english_errors


    def validate_plan(self):
        errors = []

        errors.extend(self.check_course_load())
        errors.extend(self.check_te_hours())
        errors.extend(self.check_cse_hours())

        print errors
        return errors

    def count_tes(self):
        tes = self.courses.filter(core_course=False).filter(cse_a=0).filter(cse_b=0).filter(cse_c=0).filter(cse_d=0)
        return tes.count()
        # tsk tsk Zulak
        # return len(tes)

    def count_cses(self):
        # list_a = len(self.courses.filter(cse_a__gt=0))
        # list_b = len(self.courses.filter(cse_b__gt=0))
        # list_c = len(self.courses.filter(cse_c__gt=0))
        # list_d = len(self.courses.filter(cse_d__gt=0))

        # return list_a + list_b + list_c + list_d
        cses = self.courses.exclude(cse_a=0, cse_b=0, cse_c=0, cse_d=0)
        return cses.count()

    def check_course_load(self):
        errors = []

        min_course_load = self.min_courses_per_term() - self.requirements.courses_per_term

        if min_course_load < 0:
            errors.append(
                ('too_few_courses', abs(min_course_load))
            )

        cse_count = self.count_cses()   
        if cse_count < 6:
            errors.append(
                ('too_few_cses', 6 - cse_count)
            )

        te_count = self.count_tes()    
        if te_count < 6:
            errors.append(
                ('too_few_tes', 6 - te_count)
            )

        return errors

    def check_te_hours(self):
        errors = []

        te_hours = self.all_te_hours()
        # math = self.te_hours('math') - self.requirements.min_math_sci
        # sci = self.te_hours('sci') - self.requirements.min_math_sci
        math = te_hours['math'] - self.requirements.min_math_sci
        sci = te_hours['sci'] - self.requirements.min_math_sci
        math_sci = math + sci

        # engsci = self.te_hours('engsci') - self.requirements.min_engsci_des
        # engdes = self.te_hours('engdes') - self.requirements.min_engsci_des
        engsci = te_hours['engsci'] - self.requirements.min_engsci_des
        engdes = te_hours['engdes'] - self.requirements.min_engsci_des
        engsci_des = engsci + engdes

        minimum_te_course_hours = (
          (math, 'math'),
          (sci, 'sci'),
          (engsci, 'engsci'),
          (engdes, 'engdes')
        )

        print minimum_te_course_hours

        for c in minimum_te_course_hours:
            shortfall, requirement = c
            error = "%s_too_low" % (requirement,)
            if shortfall < 0:
                errors.append((error, str(abs(shortfall))))

        if math_sci < self.requirements.extra_math_sci:
            errors.append(('math_sci_too_low',
                           str(self.requirements.extra_math_sci - math_sci)
                          ))

        if engsci_des < self.requirements.extra_engsci_des:
            errors.append(('engsci_des_too_low',
                           str(self.requirements.extra_engsci_des - engsci_des)
                          ))

        return errors

    def check_cse_hours(self):
        def other_cse_counts(list_a, list_c, list_d):
            a = max(list_a, 0)
            c = max(list_c, 0)
            return a + c + list_d

        errors = []
        # extra_a = self.cse_count('a') - self.requirements.cse_a
        # extra_b = self.cse_count('b') - self.requirements.cse_b
        # extra_c = self.cse_count('c') - self.requirements.cse_c
        # extra_d = (other_cse_counts(extra_a, extra_c, self.cse_count('d')) -
        #            self.requirements.cse_other)
        extra_a = self.quicker_cse_count('a') - self.requirements.cse_a
        extra_b = self.quicker_cse_count('b') - self.requirements.cse_b
        extra_c = self.quicker_cse_count('c') - self.requirements.cse_c
        extra_d = (other_cse_counts(extra_a, extra_c, self.quicker_cse_count('d')) -
                   self.requirements.cse_other)

        cse_selections = (
            (extra_a, 'list_a'),
            (extra_b, 'list_b'),
            (extra_c, 'list_c'),
            (extra_d, 'list_other'),
        )

        for c in cse_selections:
            number, category = c
            if number < 0:
                error = "%s_too_low" % category
                errors.append(
                    (error, abs(number))
                )

        return errors

class PlannedCourse(models.Model):
    plan = models.ForeignKey(Plan)
    course = models.ForeignKey(Course)
    term = models.PositiveIntegerField(choices=TERMS)
    created_at = models.DateTimeField(auto_now_add = True)
    updated_at = models.DateTimeField(auto_now = True)

    class Meta:
        unique_together = (("plan", "course"),)

    def __unicode__(self):
        return self.course.short_form

    @permalink
    def get_absolute_url(self):
        return ('planned_course_detail', [str(self.plan.id), str(self.id)])

    @property
    def offerings(self):
        term_code = self.plan.student.get_term_code(self.term)
        return Offering.objects.filter(term=term_code, course=self.course)

def log_exception(*args, **kwds):
    """Django signal handler to log an exception."""
    import sys
    import logging

    cls, err = sys.exc_info()[:2]
    logging.exception('Exception in request: %s: %s', cls.__name__, err)

got_request_exception.connect(log_exception)

def add_course_to_schedule(sender, instance, created, **kwargs):
    """
    If a new planned_course is created, add any unique scheduling data
    to the student' schedule. If only a single lecture, tutorial or lab
    exists, then add it to the plan.
    """
    if created:
        planned_course = instance
        schedule = planned_course.plan.student.schedule
        offerings = planned_course.offerings
        for offering_type in OFFERING_TYPE_CHOICES:
            type_offerings = offerings.filter(type=offering_type[0])
            if len(type_offerings) == 1:
                schedule.offerings.add(type_offerings.get())
        schedule.save()

def remove_course_from_schedule(sender, instance, **kwargs):
    """
    Remove all schedulings from a student's schedule when the course
    if removed from a student's plan.
    """
    planned_course = instance
    course = planned_course.course
    schedule = planned_course.plan.student.schedule
    offerings_to_remove = schedule.offerings.filter(course=course)
    schedule.offerings.remove(*offerings_to_remove)


post_save.connect(add_course_to_schedule, sender=PlannedCourse)
post_delete.connect(remove_course_from_schedule, sender=PlannedCourse)

